/*
 License
 
 Copyright © 2012 Kevin Kimmich
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights to 
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 the Software, and to permit persons to whom the Software is furnished to do so, 
 subject to the following conditions:
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <signal.h>
#include <ctype.h>
#include "crcore.h"
#include "messaging.h"
#include "kuato_http.h"
#include "kuato_http_operations.h"
#include "kuato_server.h"
#include "kuato_client.h"
#include "method.h"


static void http_process_method(http_server_object* h,connection_context* ctx, method* m,request* r, void(*parse_header)(connection_context*, method*));

void http_process_read(http_server_object* h,connection_context* ctx, method* m,request* req,void(*parse_header)(connection_context*, method*),int wait_time)
{
	int done=0;
	while(!done)
	{
		int bytes_read=0;
		io_result result = io_null;
		
		context_read(ctx,&result,&bytes_read);
		
		if(result == io_closed)
			break;
		
		if(bytes_read)
		{
			http_process_method(h,ctx,m,req,parse_header);
			
			switch(m->status)
			{
				default:
					done=1;
				break;
				case METHOD_WAIT_HEADER:
				case METHOD_WAIT_BODY:
				case METHOD_RUNNING:
				break;
			};
		}
		else
		{
			stream_to_stream(&req->body,&ctx->read_stream);
			if(m->status==METHOD_WAIT_BODY)
				m->status=METHOD_COMPLETE;
			return;
		}
		
		if(result==io_closed)
			break;
	}
}

void do_http_response(method* m, const char* mime_type, result_code http_result, stream* headers, stream* body)
{	
	if(m->rule && m->rule->htype != discrete)
	{
		printf("Using do_http_response in a chunked handler.\n");
		exit(-1);
	}	
	
	m->req.status = http_result;
	
	stream_printf(&m->resp.s,"HTTP/1.1 %d %s\r\n",m->req.status.code,m->req.status.information_string);
	stream_printf(&m->resp.s,"%s\r\n",CONFIG_SERVER_RESPONSE_HEADER_STRING);
	
	stream_to_stream(&m->resp.body,body);
	
	method_do_compression(m);
	stream_printf(&m->resp.s,"CONTENT-LENGTH: %lu\r\n",stream_length(&m->resp.body));
	stream_printf(&m->resp.s,"CONTENT-TYPE: %s\r\n",mime_type);
	if(headers)
		stream_to_stream(&m->resp.s,headers);
	stream_printf(&m->resp.s,"\r\n");
	
	stream_to_stream(&m->resp.s,&m->resp.body);
	
	m->status = METHOD_COMPLETE;
}

void http_reject( method* m )
{
	stream body;
	stream_init(&body,UNBOUNDED);
	
	m->req.status = HTTP_INTERNAL_SERVER_ERROR;
	
	stream_printf(&body,"<html><head/><body><h>%s: %d %s</h></body></html>",m->req.uri.path,m->req.status.code,m->req.status.information_string);
	
	do_http_response(m,"text/html",HTTP_INTERNAL_SERVER_ERROR,NULL,&body);
	
	stream_cleanup(&body);
}

void http_handle_get(domain_object* d, method* m)
{	
	stream body;
	stream_init(&body,UNBOUNDED);
	
	stream path;
	stream_init(&path,UNBOUNDED);
	if(strcmp(m->req.uri.path,"/")==0)
		stream_printf(&path,"%s/%s",d->root,d->default_file);
	else
		stream_printf(&path,"%s/%s",d->root,m->req.uri.path);
	
	char* path_name = stream_to_string(&path,NULL);
	
	if(stream_from_filename(&body,path_name)>=0)
	{
		const char *mime_type=get_mime_type_from_method(m);
		
		char mod_time[LOCAL_BUF_SIZE];
		struct stat st;
		if(stat(path_name,&st)==0)
		{
			struct tm* t = gmtime(&st.st_mtime);
			strftime(mod_time,LOCAL_BUF_SIZE,"%a, %d %b %Y %T GMT",t);
		}
		
		stream headers;
		stream_init(&headers,UNBOUNDED);
		stream_printf(&headers,"LAST-MODIFIED: %s\r\n",mod_time);
		
		do_http_response(m,(char*)mime_type,HTTP_OK,&headers,&body);
	}
	else
	{
		m->req.status = HTTP_NOT_FOUND;
		
		stream_printf(&body,"<html><head/><body><h>%s: %d %s</h></body></html>",m->req.uri.path,m->req.status.code,m->req.status.information_string);
		
		do_http_response(m,"text/html",HTTP_NOT_FOUND,NULL,&body);
	}
	
	stream_cleanup(&body);
}

static void http_process_method(http_server_object* h,connection_context* ctx, method* m, request* r,void(*parse_header)(connection_context*, method*))
{
	if(m->status==METHOD_WAIT_HEADER)
	{
		stream_to_stream(&r->s,&ctx->read_stream);
		
		int pos = stream_strstr(&r->s,CRLFCRLF);
		
		if(pos >= 0)
		{
			r->header = malloc(pos+1);
			r->header[pos]=0;
			
			stream_to_memory(&r->s,r->header,pos);
			
			stream_to_memory(&r->s,NULL,4);
			stream_to_stream(&r->body,&r->s);
			
			parse_header(ctx,m);
		}
	}
				
	if(m->status==METHOD_WAIT_BODY)
	{
		stringbuffer* value = hash_string_lookup(&r->dict,"TRANSFER-ENCODING");
		
		if(value && (strcmp(sb_get_buffer(value),"chunked")==0))
		{
			//there's still some weirdness with chunked downloads
			//sometimes there's an extra 0x0A before the number of bytes
			//sometimes there's only a CRLF
			stream_to_stream(&r->body,&ctx->read_stream);
			for(;;)
			{
				int len;
				if(r->chunk_length)
				{
					len=r->chunk_length;
				}
				else
				{
					len = stream_strstr(&r->body,CRLF);
					if(len<0)
					{
						return;
					}
					
					if(len)
					{
						stream_to_stream_length(&r->s,&r->body,len);
						int strl = 0;
						char* buf = stream_to_string(&r->s,&strl);
						//sometimes there is not enough data in the body stream
						//but that should be ok, not cuase a seg fault
						//however, there needs to be some other state
						//to work correctly
						len = strtol(buf,NULL,16);
						free(buf);
					}
					else
					{
						stream_to_memory(&r->body,NULL,2);
						return;
					}
					stream_to_memory(&r->body,NULL,2);
				}
				
				if(len)
				{
					int l=stream_to_stream_length(&r->chunk_body,&r->body,len);
					if(l==len)
					{
						stream_to_memory(&r->body,NULL,2);
						r->chunk_length=0;
					}
					else
					{
						r->chunk_length=len-l;
					}
				}
				else
				{
					stream_to_stream(&r->body,&r->chunk_body);
					m->status=METHOD_REQUEST_COMPLETE;
					break;
				}
				
				if(stream_length(&r->body)==0)
					break;
			}
		}
		else
		{
			stream_to_stream(&r->body,&ctx->read_stream);
			int stream_len = stream_length(&r->body);
			stringbuffer* value = hash_string_lookup(&r->dict,"CONNECTION");
			if(value && !strcasecmp(sb_get_buffer(value),"close") && (r->content_length==0))
			{
				//no content length was provided,
				//wait until the socket is shutdown
			}
			else
			{
				if((stream_len == r->content_length))
				{
					m->status=METHOD_REQUEST_COMPLETE;
				}
				else
				{
					if(stream_len > r->content_length)
					{
						if((stream_len - r->content_length)!=2)
						{
							THROW(HTTP_BODY_ERROR,"Body length '%u' exceeds expected content length '%u'",stream_len,r->content_length);
						}
						else
						{
							//Linux Firefox provides two extra bytes, so let that slide
							m->status=METHOD_REQUEST_COMPLETE;
						}
					}
				}
			}
		}
	}
}

void* handle_http_connection(int argc, void** argv)
{
	connection_context* ctx = argv[0];
	http_server_object* h = ctx->http_srv;

	for(;;)
	{
		int wait_time = FOREVER;
		
		if(ctx->methods.head.next)
			wait_time=0;
		
		if(ctx->m==NULL)
			ctx->m = create_server_method(h,ctx);

		int req_err = 0;
		exception_env_t e=INIT_EXCEPTION;
		TRY
		{
			http_process_read(h,ctx,ctx->m,&ctx->m->req,parse_request_header,wait_time);
		}
		CATCH(e)
		{
			//print_exception("SERVER:",&e);
			//cleanup uri handler
			//ctx->m->status = METHOD_REQUEST_ERROR;
			req_err=1;
		}
		FINALIZE
		
		if(req_err)
			break;//socket closed
		
		if(ctx->m->status == METHOD_REQUEST_COMPLETE)
		{
			stringbuffer* host = hash_string_lookup(&ctx->m->req.dict,"HOST");
			domain_object* d = http_server_lookup_domain(h,"localhost");
			if(host)
			{
				d = http_server_lookup_domain(h,sb_get_buffer(host));
			}
			
			if(!d)
			{
				ctx->m->req.status = HTTP_BAD_REQUEST;
				method* m = ctx->m;
				stream_printf(&m->resp.body,"<html><head/><body><h>Unknown host '%s' %s</h></body></html>",host ? sb_get_buffer(host):"(NULL)");
				stream_printf(&m->resp.s,"HTTP/1.1 %d %s\r\n",m->req.status.code,m->req.status.information_string);
				stream_printf(&m->resp.s,"CONTENT-LENGTH: %lu\r\n",stream_length(&m->resp.body));
				stream_printf(&m->resp.s,"CONTENT-TYPE: %s\r\n","text/html");
				stream_printf(&m->resp.s,"\r\n");
				
				stream_to_stream(&m->resp.s,&m->resp.body);
				
				m->status = METHOD_COMPLETE;
				
				list_add_tail(&ctx->methods,&m->node);
				ctx->m=NULL;
			}
			else
			{
				ctx->m->status=METHOD_RUNNING;
				ctx->m->d = d;
			}
			
			if(ctx->m)
			{
				http_match_rule_t* rule = get_match_rule(d,ctx->m);
				if(rule==NULL)
				{
					if(ctx->m->req.mtype==GET)
					{
						list_add_tail(&ctx->methods,&ctx->m->node);
						http_handle_get(d,ctx->m);
						ctx->m = NULL;
					}
					else
					{
						ctx->m->req.status = HTTP_METHOD_NOT_ALLOWED;
						method* m = ctx->m;
						stream_printf(&m->resp.body,"<html><head/><body><h>%d %s</h></body></html>",m->req.status.code,m->req.status.information_string);
						stream_printf(&m->resp.s,"HTTP/1.1 %d %s\r\n",m->req.status.code,m->req.status.information_string);
						stream_printf(&m->resp.s,"CONTENT-LENGTH: %lu\r\n",stream_length(&m->resp.body));
						stream_printf(&m->resp.s,"CONTENT-TYPE: %s\r\n","text/html");
						stream_printf(&m->resp.s,"\r\n");
						
						stream_to_stream(&m->resp.s,&m->resp.body);
						
						list_add_tail(&ctx->methods,&m->node);
						ctx->m=NULL;
					}
				}
				else
				{
					list_add_tail(&ctx->methods,&ctx->m->node);
					if(rule->htype==discrete)
						rule->handle_request(ctx->m);
					else
						chunked_handler_func(rule,ctx->m);
					ctx->m=NULL;
				}
			}
		}
		else
		{
			if(ctx->m->status == METHOD_HEADER_ERROR)
			{
				ctx->m->req.status = HTTP_BAD_REQUEST;
				method* m = ctx->m;
				stream_printf(&m->resp.body,"<html><head/><body><h>%d %s</h></body></html>",m->req.status.code,m->req.status.information_string);
				stream_printf(&m->resp.s,"HTTP/1.1 %d %s\r\n",m->req.status.code,m->req.status.information_string);
				stream_printf(&m->resp.s,"CONTENT-LENGTH: %lu\r\n",stream_length(&m->resp.body));
				stream_printf(&m->resp.s,"CONTENT-TYPE: %s\r\n","text/html");
				stream_printf(&m->resp.s,"\r\n");
				
				stream_to_stream(&m->resp.s,&m->resp.body);
				
				list_add_tail(&ctx->methods,&m->node);
				ctx->m=NULL;
			}
		}
		
		if(ctx->methods.head.next)
		{
			method* tmp = (method*)ctx->methods.head.next;
			
			if(tmp->rule && tmp->rule->htype==chunked)
			{
				//for chunked methods there
				//is only one per connection
				if(tmp->status==METHOD_COMPLETE)
				{
					list_remove(&ctx->methods,&tmp->node);
					method_destroy(tmp);
					break;
				}
			}
			else
			{
				if(tmp->status==METHOD_COMPLETE)
				{
					io_result result;
					int bytes_sent;
					stream_to_stream(&ctx->write_stream,&tmp->resp.s);
					context_wait_write(ctx,FOREVER,&result,&bytes_sent);
					list_remove(&ctx->methods,&tmp->node);
					method_destroy(tmp);
				}
			}
		}
	}
	
	if(ctx->m)
		method_destroy(ctx->m);
	
	while(ctx->methods.head.next)
	{
		method* m = (method*)ctx->methods.head.next;
		if(m->status==METHOD_COMPLETE)
		{
			list_remove(&ctx->methods,&m->node);
			method_destroy(m);
		}
		else
		{
			usleep(1000);
		}
	}
	
	destroy_context(ctx);
	
	return NULL;
}

connection_context* open_http_server(http_server_object* h,struct sockaddr_in address, int max_conn, const char* cert_file, const char* key_file)
{
	connection_context* server_context;
	
	if(cert_file)
	{
#ifdef ENABLE_SSL
		h->sslctx = messaging_create_server_ssl_context(cert_file,key_file);
		server_context = create_ssl_listener_context(address,max_conn,h);
#else
		printf("A certificate file is being passed to open_http_server(), but ENABLE_SSL is false.");
		assert(0);
#endif
	}
	else
		server_context = create_listener_context(address,max_conn,h);
	
	return server_context;
}

void close_http_server(connection_context* ctx)
{
	destroy_context(ctx);
}
